
package com.google.code.iproc;

import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.dom4j.io.SAXReader;

import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.ColumnText;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.PdfTemplate;
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import com.itextpdf.text.pdf.parser.RenderListener;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import com.itextpdf.text.pdf.parser.Vector;

public class PDF_Amender extends Task {

	private int nThreads = Runtime.getRuntime().availableProcessors();

	private File textSrcDir;

	private File textDstDir;

	private File textList;
	
	private File fontDir;

	private String filter;
	
	private String paperFormat = "A4";

	public PDF_Amender() {
		super();
	}

	public void setThreadCount( final int x ) {
		this.nThreads = x;
	}

	public void setTextList( final File file ) {
		this.textList = file;
	}

	public void setTextSrcDir( final File dir ) {
		this.textSrcDir = dir;
	}

	public void setTextDstDir( final File dir ) {
		this.textDstDir = dir;
	}

	public void setFontDir( final File dir ) {
		this.fontDir = dir;
	}
	
	public void setFilter( final String s ) {
		this.filter = s;
	}
	
	public void setPaperFormat( final String f ) {
		this.paperFormat = f;
	}

	public void validate() throws BuildException {
		// TODO
	}

	private final AtomicInteger filesCount = new AtomicInteger();
	private final AtomicLong inFilesSize = new AtomicLong();
	private final AtomicLong outFilesSize = new AtomicLong();
	private final AtomicInteger nPagesTotal = new AtomicInteger();

	@SuppressWarnings( "unchecked" )
	@Override
	public void execute() throws BuildException {
		validate();

		final ExecutorService es = Executors.newFixedThreadPool( nThreads );

		try {
			baseFont[0] = BaseFont.createFont( new File( fontDir, "DejaVuSerifCondensed.ttf" ).getAbsolutePath(), BaseFont.IDENTITY_H, true );
			baseFont[1] = BaseFont.createFont( new File( fontDir, "DejaVuSerifCondensed-Bold.ttf" ).getAbsolutePath(), BaseFont.IDENTITY_H, true );
			baseFont[2] = BaseFont.createFont( new File( fontDir, "DejaVuSerifCondensed-Italic.ttf" ).getAbsolutePath(), BaseFont.IDENTITY_H, true );
			baseFont[3] = BaseFont.createFont( new File( fontDir, "DejaVuSerifCondensed-BoldItalic.ttf" ).getAbsolutePath(), BaseFont.IDENTITY_H, true );
			final float fontSize = 7f;
			font[0] = new Font( baseFont[2], fontSize );
			font[1] = new Font( baseFont[0], fontSize );

			com.itextpdf.text.Document.compress = true;

			// work with true RDF here
			final SAXReader reader = new SAXReader();
			final Document indexDoc = reader.read( textList );
			final Element indexRoot = indexDoc.getRootElement();
			for ( final Element descEl : (List<Element>) indexRoot.elements() ) {
				final String aboutUrl = descEl.attributeValue( RDF_ABOUT_QNAME );
				if ( filter == null || aboutUrl != null && aboutUrl.startsWith( filter ) ) {
					es.submit( new Worker( descEl ) );
				}
			}
		} catch ( final Exception e ) {
			throw new BuildException( "When planning processing " + e.toString(), e );
		}

		es.shutdown();
		while ( !Thread.interrupted() && !es.isTerminated() ) {
			try {
				es.awaitTermination( 20, TimeUnit.MILLISECONDS );
			} catch ( final InterruptedException e ) {
				Thread.currentThread().interrupt();
			}
		}

		getProject().log( "Successfully processed " + filesCount.intValue() + " files, total " + nPagesTotal + " PDF pages" );
		getProject().log( "Total input size: " + inFilesSize.longValue() + " B" );
		getProject().log( "Total output size: " + outFilesSize.longValue() + " B" );
		getProject().log(	"Average size increase: " + ( outFilesSize.longValue() - inFilesSize.longValue() )
									/ filesCount.doubleValue() + " B/file" );
		getProject().log(	"Total size increase: " + 100 * ( outFilesSize.longValue() - inFilesSize.longValue() )
									/ inFilesSize.doubleValue() + " %" );
	}

	private static final Namespace RDF_NS = Namespace.get( "rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#" );
	private static final Namespace TOC_NS = Namespace.get( "toc", "urn:cz:mathan:toc-1.0" );

	private static final QName RDF_ABOUT_QNAME = QName.get( "about", RDF_NS );
	private static final QName TOC_SOURCE_QNAME = QName.get( "source", TOC_NS );
	private static final QName TITLE_QNAME = QName.get( "title" );
	private static final QName KEYWORD_QNAME = QName.get( "keyword" );
	private static final QName SUBJECT_QNAME = QName.get( "subject" );
	private static final QName REF1_QNAME = QName.get( "ref1", TOC_NS );
	private static final QName REF2_QNAME = QName.get( "ref2", TOC_NS );
	private static final QName REF3_QNAME = QName.get( "ref3", TOC_NS );

	private final BaseFont[] baseFont = new BaseFont[4];
	private final Font[] font = new Font[4];

	private static final float tol = 1f;

	private class Worker implements Callable<Void> {

		private final Element descEl;

		public Worker( final Element el ) {
			this.descEl = el;
		}

		@SuppressWarnings( "unchecked" )
		@Override
		public Void call() throws Exception {
			final String about = descEl.attributeValue( RDF_ABOUT_QNAME );
			final String aboutPdf = about.replaceAll( "\\.doc$", ".pdf" );
			final String source = descEl.attributeValue( TOC_SOURCE_QNAME );
			final String sourcePdf = ( source == null || source.isEmpty() ) ? aboutPdf : source.replaceAll( "\\.doc$", ".pdf" );
			final File inputFile = new File( textSrcDir, sourcePdf );
			final File outputFile = new File( textDstDir, aboutPdf );
			outputFile.getParentFile().mkdirs();
			getProject().log( about + ": " + inputFile + " -> " + outputFile );

			try {
				final PdfReader reader = new PdfReader( inputFile.getAbsolutePath() );
				final PdfStamper stamper = new PdfStamper( reader, new FileOutputStream( outputFile ) );
				try {
					stamper.getWriter().setCompressionLevel( 9 );
					stamper.setRotateContents( false );
					
					final PdfReaderContentParser parser = new PdfReaderContentParser( reader );

					final HashMap<String, String> info = reader.getInfo();
					getProject().log( info.toString(), Project.MSG_VERBOSE );
					info.put( "Author", stringValue( descEl.element( REF1_QNAME ) ).replaceAll( ":$", "" ) );
					info.put( "Title", stringValue( descEl.element( TITLE_QNAME ) ) );
					info.put( "Subject", "Current Research Information Systems" );
					info.put( "Keywords", "CRIS 2012" );
					stamper.setMoreInfo( info );

					final Rectangle paperSize = PageSize.getRectangle( paperFormat );
					final float lx = 3.4f / 2.54f * 72f;
					final float rx = paperSize.getWidth() - lx;
					final float by = 3.4f / 2.54f * 72f;
					final float font0Size = font[0].getSize();
					final float by1 = by - 1.2f * font0Size;
					final float tyLimit = paperSize.getHeight() - 2.4f * font0Size;
					final float ty = paperSize.getHeight() - by;
					final BaseColor headerFooterLineColor = BaseColor.BLACK;
					final float headerFooterLineThickness = 0.05f;

					final PdfTemplate template = PdfTemplate.createTemplate(	stamper.getWriter(), paperSize.getWidth(),
																				paperSize.getHeight() );

					// page header
					final PdfPTable table = new PdfPTable( 1 );
					table.setTotalWidth( rx - lx );
					table.setLockedWidth( true );
					table.setHorizontalAlignment( PdfPTable.ALIGN_LEFT );
					final PdfPCell defaultCell = table.getDefaultCell();
					defaultCell.setBorder( Rectangle.BOTTOM );
					defaultCell.setBorderColor( headerFooterLineColor );
					defaultCell.setBorderWidth( headerFooterLineThickness );
					defaultCell.setLeading( 0f, 1.4f );
					defaultCell.setPadding( 0f );
					defaultCell.setPaddingBottom( 3f );
					final Paragraph para = new Paragraph();
					//					para.setLeading( 1.5f * font0Size );
					para.setHyphenation( null );
					para.setKeepTogether( true );
					para.setAlignment( com.itextpdf.text.Element.ALIGN_LEFT + com.itextpdf.text.Element.ALIGN_BOTTOM );
					para.setFont( font[1] );
					para.add( descEl.element( REF1_QNAME ).getStringValue().trim() + " " );
					para.setFont( font[0] );
					para.add( descEl.elementText( REF2_QNAME ) );
					para.setFont( font[1] );
					final String ref3 = descEl.elementText( REF3_QNAME );
					if ( ref3 != null ) {
						para.add( " " + ref3 + "." );
					}
					table.addCell( para );
					final float ty1 = Math.min( ty + table.getTotalHeight(), tyLimit );
					table.writeSelectedRows( 0, -1, lx, ty1, template );
					final Rectangle2D.Float headerBounds = new Rectangle2D.Float( lx, ty1, table.getTotalWidth(), table.getTotalHeight() );

					//					final ColumnText ct = new ColumnText( template );
					//					ct.setSimpleColumn( para, lx, ty1, rx, ty2, 1.5f * font0Size, com.itextpdf.text.Element.ALIGN_LEFT + com.itextpdf.text.Element.ALIGN_BOTTOM );
					//					ct.go();
					//					drawThinLine( template, lx, rx, ty );

					// page footer
					final Image img = Image.getInstance( this.getClass().getResource( "icons/cc-by-nd.png" ) );
					final float imgScaleX = 32.0f;
					final float imgScaleY = imgScaleX * 141f / 403f;
					template.addImage( img, imgScaleX, 0, 0, imgScaleY, lx, by );
					final Paragraph p1 = new Paragraph();
					p1.setKeepTogether( true );
					p1.setHyphenation( null );
					p1.setAlignment( com.itextpdf.text.Element.ALIGN_LEFT + com.itextpdf.text.Element.ALIGN_BOTTOM );
					p1.setFont( font[1] );
					p1.add( "This work is licensed under a Creative Commons Attribution-NoDerivs 3.0 Unported License." );
					ColumnText.showTextAligned( template, com.itextpdf.text.Element.ALIGN_LEFT, p1, lx, by1, 0 );
					final Rectangle2D.Float footerBounds = new Rectangle2D.Float( lx, by1, rx - lx, p1.getLeading() );
					template.saveState();
					try {
						template.setLineWidth( headerFooterLineThickness );
						template.setColorStroke( headerFooterLineColor );
						template.stroke();
						template.moveTo( lx, by );
						template.lineTo( rx, by );
						template.closePathStroke();
					} finally {
						template.restoreState();
					}

					// flush the template
					stamper.getWriter().releaseTemplate( template );

					final StringBuilder nonStandardPageSizeWarning = new StringBuilder();
					final StringBuilder overlappingHeaderAndContentsWarning = new StringBuilder();
					final StringBuilder emptyPageWarning = new StringBuilder();
					
					final int nPages = reader.getNumberOfPages();
					for ( int i = 1; i <= nPages; ++i ) {
						final PdfContentByte pageCanvas = stamper.getUnderContent( i );
						pageCanvas.addTemplate( template, 0, 0 );
						final Phrase phraseP = new Phrase( "" + i + "", font[1] );
						ColumnText.showTextAligned( pageCanvas, com.itextpdf.text.Element.ALIGN_RIGHT, phraseP, rx, by1, 0 );

						final Rectangle pageSize = reader.getPageSize( i );
						if ( Math.abs( pageSize.getHeight() - paperSize.getHeight() ) > tol
								|| Math.abs( pageSize.getWidth() - paperSize.getWidth() ) > tol ) {
							nonStandardPageSizeWarning.append( " page " + i + " (" + pageSize.getWidth() + "x" + pageSize.getHeight() + ")" );
						}
					}
					nPagesTotal.addAndGet( nPages );
					
					if ( nonStandardPageSizeWarning.length() > 0 ) {
						getProject().log( PDF_Amender.this, about + ": pages with non standard paper size (" + paperFormat + ": " + paperSize.getWidth() + "x" + paperSize.getHeight() + "):" + nonStandardPageSizeWarning, Project.MSG_WARN );
					}
					if ( overlappingHeaderAndContentsWarning.length() > 0 ) {
						getProject().log( PDF_Amender.this, about + ": overlapping page contents and header:" + overlappingHeaderAndContentsWarning, Project.MSG_WARN );
					}
					if ( emptyPageWarning.length() > 0 ) {
						getProject().log( PDF_Amender.this, about + ": empty pages:" + emptyPageWarning, Project.MSG_WARN );
					}
				} finally {
					stamper.close();
				}
				filesCount.incrementAndGet();
				inFilesSize.addAndGet( inputFile.length() );
				outFilesSize.addAndGet( outputFile.length() );
			} catch ( final Exception e ) {
				getProject().log( PDF_Amender.this, "error: " + e, e, Project.MSG_ERR );
				e.printStackTrace();
			}
			return null;
		}

	}
	
	private static String jointString( final List<Element> els, final String sep ) {
		final StringBuilder sb = new StringBuilder();
		boolean empty = true;
		for ( final Element el : els ) {
			if ( !empty ) {
				sb.append( sep );
			} else {
				empty = false;
			}
			sb.append( el.getStringValue() );
		}
		return sb.toString();
	}
	
	private static String stringValue( final Element el ) {
		return ( el != null ) ? el.getStringValue().trim() : "";
	}
}
